package top.kaoshanji.p2ch14a;

import java.io.*;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.Map;
import java.util.Queue;

/**
 * 代码清单-p2ch14a-397页b：BasicDB的实现
 * author kaoshanji
 * ^_^
 * create time 2025/2/21 15:37
 */
public class BasicDB {

    // 一份数据占据的最大长度
    private static final int MAX_DATA_LENGTH = 1020;

    // 补白字节
    private static final byte[] ZERO_BYTES = new byte[MAX_DATA_LENGTH];

    // 数据文件后缀
    private static final String DATA_SUFFIX = ".data";

    // 元数据文件后缀，包括索引和空白空间数据
    private static final String META_SUFFIX = ".meta";

    // 一份数据，键：k1、值：v1；存储在数据文件字节长度位置：2232
    // 索引信息，键 -> 值 在 .data 文件中的位置
    // 键：数据的键 --> k1
    // 值：存储在数据文件字节长度位置 --> 2232
    private Map<String, Long> indexMap;

    // 空白空间，值为在 .data 文件中的位置
    // 每个元素对应一份数据
    // 每个元素的值是该数据补白的数量
    private Queue<Long> gaps;

    // 值数据文件
    // 键：存储在数据文件字节长度位置 --> 2232
    // 值：数据的值 --> v1
    private RandomAccessFile db;

    // 元数据文件
    private File metaFile;

    // ---------------公共方法-----------------------

    // 构造器用来创建对象，这里为上述属性字段赋值，后续方法调用时处理逻辑，也就是操作上述属性字段
    // 每个对象都有一份属于自己的独立的属性字段和方法

    /**
     * 构造器
     * @param path 数据库文件所在的目录
     * @param name 数据库名称
     * @throws IOException
     */
    public BasicDB(String path, String name) throws IOException {

        File dataFile = new File(path + name + DATA_SUFFIX);

        metaFile = new File(path + name + META_SUFFIX);
        db = new RandomAccessFile(dataFile, "rw");

        // 如果文件存在..就使用文件里的数据初始化 indexMap、gaps
        if (metaFile.exists()) {
            loadMeta();
        } else {
            indexMap = new HashMap<>();
            gaps = new ArrayDeque<>();
        }

    }

    // 保存键值对，键为 String 类型，值为 byte 数组
    // 因为是顺序写入，那么，保存的时候，该数据肯定是最新的啦...
    public void put(String key, byte[] value) throws IOException {
        Long index = indexMap.get(key);
        if (index == null) {
            index = nextAvailablePos();
            indexMap.put(key, index);
        }
        writeData(index, value);
    }

    // 根据键获取值，如果键不存在，返回 null
    public byte[] get(String key) throws IOException {
        Long index = indexMap.get(key);
        if (index != null) {
            return getData(index);
        }
        return null;
    }

    // 根据键删除
    public void remove(String key) {
        Long index = indexMap.remove(key); // 索引里面没有这个键了
        if (index != null) {
            gaps.offer(index);
        }

    }

    // 确保将所有数据保存到文件
    public void flush() throws IOException {
        saveMeta();
        db.getFD().sync();
    }

    // 关闭数据库
    public void close() throws IOException {
        flush();
        db.close();
    }


    // ---------------私有方法-----------------------

    // 为值找一个存储位置
    private long nextAvailablePos() throws IOException {
        if (!gaps.isEmpty()) {
            return gaps.poll();
        } else {
            return db.length();
        }
    }

    /**
     * 保存数据
     * @param pos 索引 数据占用的大小累计
     * @param data 数据
     * @throws IOException
     */
    private void writeData(long pos, byte[] data) throws IOException {
        if (data.length > MAX_DATA_LENGTH) {
            throw new IllegalArgumentException("maximum allowed length is "
                    + MAX_DATA_LENGTH + ", data length is " + data.length);
        }

        db.seek(pos); // 更改当前文件指针到 pos
        db.writeInt(data.length); // 数据的大小
        db.write(data); // 数据的值
        db.write(ZERO_BYTES, 0, MAX_DATA_LENGTH - data.length); // 补白字节
    }

    /**
     * 获取数据
     * @param pos 数据索引的位置
     * @return
     * @throws IOException
     */
    private byte[] getData(long pos) throws IOException {
        db.seek(pos);

        int length = db.readInt(); // 数据的大小
        byte[] data = new byte[length]; // 容纳数据
        db.readFully(data); // 读取数据内容
        return data;
    }

    // 保存元数据
    private void saveMeta() throws IOException {
        // 输出....
        DataOutputStream out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(metaFile)));
        try {
            saveIndex(out);
            saveGaps(out);
        } finally {
            out.close();
        }
    }

    // 加载元数据
    private void loadMeta() throws IOException {
        // 输入....
        DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream(metaFile)));
        try {
            loadIndex(in);
            loadGaps(in);
        } finally {
            in.close();
        }
    }

    /**
     * 保存索引
     * 将 HashMap 里的数据写入到 文件里
     * @param out DataOutputStream
     * @throws IOException
     */
    private void saveIndex(DataOutputStream out) throws IOException {
        out.writeInt(indexMap.size());

        for (Map.Entry<String, Long> entry: indexMap.entrySet()) {
            out.writeUTF(entry.getKey());
            out.writeLong(entry.getValue());
        }
    }

    /**
     * 加载索引
     * 将文件里的数据初始化到 HashMap
     * @param in DataInputStream
     * @throws IOException
     */
    private void loadIndex(DataInputStream in) throws IOException {
        int size = in.readInt();
        indexMap = new HashMap<>((int) (size / 0.75f) + 1, 0.75f);
        for (int i = 0; i < size; i++) {
            String key = in.readUTF();
            long index = in.readLong();
            indexMap.put(key, index);
        }
    }

    // 保存空白空间信息
    private void saveGaps(DataOutputStream out) throws IOException {
        out.writeInt(gaps.size());
        for (Long pos : gaps) {
            out.writeLong(pos);
        }
    }

    // 加载空白空间
    private void loadGaps(DataInputStream in) throws IOException {
        int size = in.readInt();
        gaps = new ArrayDeque<>(size);
        for (int i = 0; i < size; i++) {
            long index = in.readLong();
            gaps.add(index);
        }
    }


}
